/*
 * Copyright 2013 JROSE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jrose.core.mongo.persistence;

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.Validate;
import org.jrose.api.annotation.persistence.MongoEntity;
import org.jrose.api.common.TimestampTouchable;
import org.jrose.core.cache.CacheManagerProvider;
import org.jrose.core.mongo.connection.MongoConnectionHolder;
import org.jrose.core.mongo.connection.MongoConnectionManager;
import org.jrose.core.util.AnnotationUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.mongodb.repository.support.MappingMongoEntityInformation;
import org.springframework.data.mongodb.repository.support.SimpleMongoRepository;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
abstract public class AbstractMongoPersistence<T, ID extends Serializable> extends CacheManagerProvider implements MongoPersistence<T, ID> {
    @Getter protected Class<T> entityClass;
    private MongoTemplate mongoTemplate;
    private MongoRepository<T, ID> repository;

    @Override
    public void afterPropertiesSet() throws Exception {
        super.afterPropertiesSet();

        ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
        //noinspection unchecked
        this.entityClass = (Class<T>) type.getActualTypeArguments()[0];
        Validate.isTrue(AnnotationUtils.isAnnotationPresent(entityClass, MongoEntity.class));
    }

    private final AtomicBoolean initialized = new AtomicBoolean(false);

    private synchronized void initializeIfNecessary() {
        if (initialized.get()) {
            return;
        }

        MongoConnectionManager connectionManager = getBean(MongoConnectionManager.class);
        MongoConnectionHolder connection = connectionManager.getMongoConnection(entityClass);

        mongoTemplate = connection.getMongoTemplate();

        String customCollectionName = null;
        if (!entityClass.isAnnotationPresent(Document.class)) {
            customCollectionName = mongoTemplate.getCollectionName(getEntityClass());
        }

        TypeInformation<T> typeInformation = ClassTypeInformation.from(getEntityClass());

        MongoMappingContext mongoMappingContext = new MongoMappingContext();
        mongoMappingContext.setApplicationContext(applicationContext);
        mongoMappingContext.afterPropertiesSet();

        //noinspection unchecked
        BasicMongoPersistentEntity<T> mongoPersistentEntity = (BasicMongoPersistentEntity<T>) mongoMappingContext.getPersistentEntity(typeInformation);
        MongoEntityInformation<T, ID> mongoEntityInformation = customCollectionName == null ?
                new MappingMongoEntityInformation<T, ID>(mongoPersistentEntity) :
                new MappingMongoEntityInformation<T, ID>(mongoPersistentEntity, customCollectionName);
        repository = new SimpleMongoRepository<>(mongoEntityInformation, mongoTemplate);

        initialized.set(true);
    }

    public MongoTemplate getMongoTemplate() {
        initializeIfNecessary();
        return mongoTemplate;
    }

    public MongoRepository<T, ID> getRepository() {
        initializeIfNecessary();
        return repository;
    }

    @Override
    public T save(T entity) {
        if (entity == null) {
            return null;
        }
        if (entity instanceof TimestampTouchable) {
            TimestampTouchable tt = (TimestampTouchable) entity;
            long current = System.currentTimeMillis();
            tt.touchCreateTime(current);
            tt.touchUpdateTime(current);
        }

        entity = getRepository().save(entity);
        return entity;
    }

    @Override
    public <S extends T> List<S> save(Iterable<S> entites) {
        if (entites != null) {
            long current = System.currentTimeMillis();
            for (T entity : entites) {
                if (entity == null) {
                    continue;
                }
                if (entity instanceof TimestampTouchable) {
                    TimestampTouchable tt = (TimestampTouchable) entity;
                    tt.touchCreateTime(current);
                    tt.touchUpdateTime(current);
                }
            }
        }
        return getRepository().save(entites);
    }

    @Override
    public T findOne(ID id) {
        if (id == null) {
            return null;
        }
        return getRepository().findOne(id);
    }

    @Override
    public boolean exists(ID id) {
        return id != null && getRepository().exists(id);
    }

    @Override
    public List<T> findAll() {
        return getRepository().findAll();
    }

    @Override
    public List<T> findAll(DBObject fieldsObject) {
        if (fieldsObject == null) {
            return findAll();
        }
        BasicQuery query = new BasicQuery(new BasicDBObject(), fieldsObject);
        return getMongoTemplate().find(query, getEntityClass());
    }

    @Override
    public List<T> findAll(Sort sort) {
        return getRepository().findAll(sort);
    }

    @Override
    public List<T> findAll(DBObject fieldsObject, Sort sort) {
        if (fieldsObject == null) {
            return findAll(sort);
        }
        Query query = new BasicQuery(new BasicDBObject(), fieldsObject);
        query = query.with(sort);
        return getMongoTemplate().find(query, getEntityClass());
    }

    @Override
    public Page<T> findAll(Pageable pageable) {
        return getRepository().findAll(pageable);
    }

    @Override
    public Page<T> findAll(DBObject fieldsObject, Pageable pageable) {
        if (fieldsObject == null) {
            return findAll(pageable);
        }
        Query query = new BasicQuery(new BasicDBObject(), fieldsObject);
        long count = count();
        List<T> list = getMongoTemplate().find(query.with(pageable), getEntityClass());
        return new PageImpl<>(list, pageable, count);
    }

    @Override
    public long count() {
        return getRepository().count();
    }

    @Override
    public long count(DBObject queryObject) {
        Query query = constructQuery(queryObject);
        if (query == null) {
            return count();
        }
        return getMongoTemplate().count(query, getEntityClass());
    }

    @Override
    public void delete(ID id) {
        if (id == null) {
            return;
        }
        getRepository().delete(id);
    }

    @Override
    public void delete(T entity) {
        getRepository().delete(entity);
    }

    @Override
    public void delete(Iterable<? extends T> entities) {
        getRepository().delete(entities);
    }

    @Override
    public void deleteAll() {
        getRepository().deleteAll();
    }

    @Override
    public void delete(DBObject queryObject) {
        Query query = constructQuery(queryObject);
        if (query == null) {
            return;
        }
        getMongoTemplate().remove(query, getEntityClass());
    }

    @Override
    public void dropCollection() {
        getMongoTemplate().dropCollection(getEntityClass());
    }

    @Override
    public T findOne(DBObject queryObject) {
        return findOne(queryObject, (DBObject) null);
    }

    @Override
    public T findOne(DBObject queryObject, DBObject fieldsObject) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return null;
        }
        return getMongoTemplate().findOne(query, getEntityClass());
    }

    @Override
    public T findOne(DBObject queryObject, Sort sort) {
        return findOne(queryObject, null, sort);
    }

    @Override
    public T findOne(DBObject queryObject, DBObject fieldsObject, Sort sort) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return null;
        }
        return getMongoTemplate().findOne(query.with(sort), getEntityClass());
    }

    @Override
    public List<T> find(DBObject queryObject) {
        return find(queryObject, (DBObject) null);
    }

    @Override
    public List<T> find(DBObject queryObject, DBObject fieldsObject) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return Collections.emptyList();
        }
        return getMongoTemplate().find(query, getEntityClass());
    }

    @Override
    public List<T> find(DBObject queryObject, Sort sort) {
        return find(queryObject, null, sort);
    }

    @Override
    public List<T> find(DBObject queryObject, DBObject fieldsObject, Sort sort) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return Collections.emptyList();
        }
        return getMongoTemplate().find(query.with(sort), getEntityClass());
    }

    @Override
    public List<T> find(DBObject queryObject, Sort sort, int limit) {
        return find(queryObject, null, sort, limit);
    }

    @Override
    public List<T> find(DBObject queryObject, DBObject fieldsObject, Sort sort, int limit) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return Collections.emptyList();
        }
        query = query.with(sort);
        if (limit > 0) {
            query = query.limit(limit);
        }
        return getMongoTemplate().find(query, getEntityClass());
    }

    @Override
    public List<T> find(DBObject queryObject, Sort sort, int limit, int skip) {
        return find(queryObject, null, sort, limit, skip);
    }

    @Override
    public List<T> find(DBObject queryObject, DBObject fieldsObject, Sort sort, int limit, int skip) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return Collections.emptyList();
        }
        query = query.with(sort);
        if (limit > 0) {
            query = query.limit(limit);
        }
        if (skip > 0) {
            query = query.skip(skip);
        }
        return getMongoTemplate().find(query, getEntityClass());
    }

    @Override
    public Page<T> find(DBObject queryObject, Pageable pageable) {
        return find(queryObject, null, pageable);
    }

    @Override
    public Page<T> find(DBObject queryObject, DBObject fieldsObject, Pageable pageable) {
        Query query = constructQuery(queryObject, fieldsObject);
        if (query == null) {
            return null;
        }
        long count = count(queryObject);
        List<T> list = getMongoTemplate().find(query.with(pageable), getEntityClass());
        return new PageImpl<>(list, pageable, count);
    }

    private Query constructQuery(DBObject queryObject) {
        return constructQuery(queryObject, null);
    }

    private Query constructQuery(DBObject queryObject, DBObject fieldsObject) {
        if (queryObject == null) {
            return null;
        }
        if (fieldsObject == null) {
            return new BasicQuery(queryObject);
        }
        return new BasicQuery(queryObject, fieldsObject);
    }
}
